<?php
/**
 * Persevere_Client
 *
 * @package    Persevere
 * @subpackage Client
 * @version    $Id: Client.php 19 2009-05-06 09:48:55Z flassen $
 * @copyright  Copyright (c) 2009 flassen[ at ] g m a i l
 * @license    http://www.opensource.org/licenses/bsd-license.php
 */
include_once('Persevere/Proxyobject.php');
include_once('Persevere/Exception.php');

class Persevere_Client {
  /**
   * URL for Persevere Server
   * @var string
   */
  private $serverUrl;
  
  /**
   * Username
   * @var string
   */
  private $user;
  
  /**
   * Password
   * @var string
   */
  private $password;

  /**
   * Http Client reference 
   * @var Zend_Http_Client
   */
  private $httpClient;
  
  /**
   * Permission levels
   * @var array
   */
  protected static $accessLevels = array(
    'none'=>'No permissions',
    'limited'=>'limited',
    'read'=>'read',
    'execute'=>'execute',
    'append'=>'append',
    'write'=>'write',
    'full'=>'full'
  );
  
  /**
   * Constructor
   * @param $url  Server
   * @param $options  not used
   * @return void
   */
  function __construct( $url, $options=array() ) {
    $info = parse_url($url);
    $this->serverUrl = $info['scheme'].'://'.$info['host'].( $info['port'] ? ':'.$info['port'] : '' ) .$info['path'];
    $this->user =  $info['user'];
    $this->password =  $info['pass'];
    // get the libs
    include_once('Zend/Http/Client.php');
    include_once('Zend/Json.php');
    // get a client
    $this->httpClient = new Zend_Http_Client( $this->serverUrl );
    // make sure that don't get CSRF-protected JSON back
    $this->httpClient->setHeaders('Accept', 'application/json');
    // @todo - cookies instead of constant http-basic-authing?
    // authentication
    if ($this->user) {
      $this->setHttpBasicAuth($this->user, $this->password);
    }
  }
  
  // {{{ 
  /**
   * Creates an object of given class
   * @param $class
   * @param $resource   array|object|string
   * @return Persevere_ProxyObject
   * @throws  Exception|Zend_Http_Client_Adapter_Exception|Persevere_Exception
   */
  function create($class, $resource) {
    try {
      if ( is_object($resource) || is_array($resource)) {
        $json = Zend_Json::encode($resource);
      } else {
        $json = $resource;
      }
      $response = $this->httpClient->setUri( $this->serverUrl.$class )->setRawData($json)->request(Zend_Http_Client::POST);
    } catch (Exception $e) {
      // @todo - handle exceptions
      throw $e;
    }
    // we should not continue if we have an error
    if ($response->isError()) {
      switch ( $response->getStatus() ) {
        case 401:
          throw new Persevere_Exception("Could not create [$class:$json] - permission denied", $response->getStatus());
          break;
        case 404:
          throw new Persevere_Exception("Could not create [$class:$json] - class does not exist", $response->getStatus());
          break;
        default:
          throw new Persevere_Exception("Could not create [$class:$json]", $response->getStatus());
      }       
    }
    return $this->createProxyObjectFromJson( $response->getBody(), $class );
  }

  /**
   * Loads an object / an array of objects from the Persevere
   * @param $query
   * @param $classHint
   * @return unknown_type
   * @throws  Exception|Zend_Http_Client_Adapter_Exception|Persevere_Exception
   */
  function load($query, $classHint=NULL) {
    try {
      // make sure we urlencode the $query
      $response = $this->httpClient->setUri( $this->serverUrl.urlencode($query) )->request(Zend_Http_Client::GET);
    } catch (Exception $e) {
      throw $e;
    }
    // we should not continue if we have an error
    if ($response->isError()) {
      throw new Persevere_Exception("Could not load query [$query]", $response->getStatus());
    }
    return $this->createProxyObjectFromJson( $response->getBody(), $classHint );
  }

  /**
   * Loads an object / an array of objects from the Persevere
   * Introduced a "classHint" because Persevere doesn't seem to always populate the id field with "{Class}/{id}"
   * but only "{id}" => i.e.loadClass provides a "class hint"
   *
   * @param $class
   * @param $filter
   * @return unknown_type
   * @throws  Exception|Zend_Http_Client_Adapter_Exception|Persevere_Exception
   */
  function loadClass( $class, $filter='' ) {
    if ( !empty($filter) ) {
      return $this->load( $class.'/'.$filter, $class );
    } else {
      return $this->load( $class, $class );
    }
  }

  /**
   * Update a resource with values
   * @param $path
   * @param $values  object|Persevere_Proxyobject|array|string
   * @param $options  not used currently
   * @return Persevere_Proxyobject
   * @throws  Exception|Zend_Http_Client_Adapter_Exception|Persevere_Exception
   */
  function update( $path, $values, $options=array() ) {
    if (is_object($values) ) {
      // must not pass on the 'id' property
      $obj = clone $values;
      unset( $obj->id );
      $json =  Zend_Json::encode( $obj );
    } elseif ( is_array($values) ) {
      // must not transfer 'id' property
      unset( $values['id'] );
      $json = Zend_Json::encode( $values );
    } elseif (is_string($values)) {
      $json = $values;
    }
    try {
      // @todo - should we urlencode $path? - i don't think persevere supports deleting with "WHERE"
      $response = $this->httpClient->setUri( $this->serverUrl.$path )->setRawData($json, 'text/json')->request(Zend_Http_Client::PUT);
    } catch (Exception $e) {
      // @todo - handle exceptions
      throw $e;
    }
    // we should not continue if we have an error
    if ($response->isError()) {
      throw new Persevere_Exception("Could not update [$path]", $response->getStatus() );
    }

    // @todo - maybe we don't need always to recreate a object, but all kinds of things can happen on the server side
     
    if (is_object($values) && is_a($values, 'Persevere_Proxyobject')) {
      // "overwrite"
      return $values = $this->createProxyObjectFromJson( $response->getBody() );
    } else {
      return $this->createProxyObjectFromJson( $response->getBody() );
    }
  }

  /**
   * Removes object by given resource/path
   * @param $path
   * @return bool#
   * @throws  Exception|Zend_Http_Client_Adapter_Exception|Persevere_Exception
   */
  function remove( $path ) {
    try {
      // @todo - should we urlencode $path? - i don't think persevere supports deleting with "WHERE"
      $response = $this->httpClient->setUri( $this->serverUrl.$path )->request(Zend_Http_Client::DELETE);
    } catch (Exception $e) {
      // @todo - handle exceptions
      throw $e;
    }
    if ($response->isError()) {
      throw new Persevere_Exception("Could not remove [$path]", $response->getStatus());
    }
    return true;
  }

  // {{{ SECURITY
  /**
   * Creates a user
   * @param $username
   * @param $password
   * @return Persevere_ProxyObject
   */
  function createUser( $username, $password ) {
    $result = $this->jsonRpc('/Class/User', 'createUser', array($username, $password) );
    // id returned is of this pattern "../User/{id}"
    list($dotdot, $user, $id) = explode('/', $result['id']);
    // @todo - do we need to return a Persevere_ProxyObject or would id suffice?
    return $this->loadClass('User', $id);   
  }
  
  /**
   * Grant access - NOT IMPLEMENTED CORRECTLY YET
   * @return bool
   */
  function grantAccess( $username, $resource, $accessLevel ) {
    if (!isset(self::$accessLevels[$accessLevel])) {
      throw Persevere_Exception("Invalid accesslevel ($accessLevel)");
    }
    // this is not right ... 
    // @todo - fix this ... 
    return $this->jsonRpc( '/Class/User', 'grantAccess', array($username, $path, $accessLevel) );
  }
  
  /**
   * Set the basic auth
   * @param $user
   * @param $password
   * @return unknown_type
   */
  function setHttpBasicAuth( $user, $password ) {
    // Zend_Http_Client base_encodes64
    $this->httpClient->setAuth( $user, $password, Zend_Http_Client::AUTH_BASIC);
  }
  
  /**
   * Helper function to authenticate, activates cookie jar
   * @param $user   
   * @param $password   
   * @param $cookieJar  bool|Zend_Http_Client_CookieJar
   * @return bool
   * @todo  cookiejar stuff is not tested
   */
  function authenticate( $user, $password, $cookieJar=true ) {
    $this->httpClient->setCookieJar($cookieJar);
    try {
      $result = $this->jsonRpc('/Class/User', 'authenticate', array($user, $password) );
      if ($result['id']) {
        return true;
      } else {
        return false;
      }
    } catch ( Exception $e ) {
      // @todo - throw or not to throw ... 
      return false;
    }
  }
  // }}} SECURITY
    
  // {{{ JSON RPC 
  /**
   * Calls a JSON-RPC method on a given path with args
   * @param $path
   * @param $method
   * @param $args
   * @return mixed
   *
   * @throws Zend_Http_Client_Adapter_Exception on connection errors
   * @throws Persevere_Exception on payload scrambling errors (i.e. invalid json etc.)
   * @throws Persevere_JsonRpcErrorException when 'error' isset in the returning payload
   */
  function jsonRpc($path, $method, $args=NULL) {
    static $rpcCallId;
    try {
      $requestBodyArray = array('method'=>$method, 'id'=>++$rpcCallId, 'params'=>$args);
      $requestBodyJson = Zend_Json::encode($requestBodyArray);
      // @todo - should we urlencode $path? 
      $response = $this->httpClient->setUri( $this->serverUrl.$path )->setRawData($requestBodyJson, 'text/json')->request(Zend_Http_Client::POST);
    } catch (Exception $e) {
      // @todo - handle exceptions
      throw $e;
    }
    if ($response->isError()) {
      throw new Persevere_Exception("Could execute jsonRpc[$path]{$method}()- [".$response->getBody()."]", $response->getStatus() );
    }
    // @todo check response id ... not neeeded in synchronous calls, i guess
    $responseBody = $response->getBody();
    $responseArray = Zend_Json::decode( $responseBody );
    if (!is_array($responseArray)) {
      throw new Persevere_Exception("jsonRpc[$path]{$method}()- did not return an array [".$response->getBody()."]", 500);
    } elseif ( $responseArray['error'] ) {
      include_once('Persevere/JsonRpcException.php');
      throw new Persevere_JsonRpcErrorException( $responseArray['error'], 500);
    }
    return $responseArray['result'];
  }
  
  // }}} JSON RPC

  // {{{ HELPER
  /**
   * Helper function that turns a returned json into one or and array of Persevere_Proxy objects
   * @param $json
   * @param $classHint
   * @return Persevere_Proxyobject|Persevere_Proxyobject[]
   */
  function createProxyObjectFromJson( $json, $classHint=NULL ) {
    // we inspect the json, crude fingerprint for a collection of objects
    if (  substr($json, 0,1) == '[' ) {
      $values = Zend_Json::decode( $json );
      if (is_array($values)) {
        $collection = array();
        foreach ($values as $k=>$v) {
          $po = new Persevere_Proxyobject( $v, $classHint );
          $po->setPersevereClient( $this );
          $collection[] = $po;
        }
        return $collection;
      } else {
        return false;
      }
    }
    // single result ... 
    $values = Zend_Json::decode( $json );
    $po = new Persevere_Proxyobject( $values, $classHint );
    $po->setPersevereClient( $this );
    return $po;
  }
  // }}} HELPER
}